﻿using System;
using System.Runtime.InteropServices;
using BGShell.Interop;
using Microsoft.Win32.SafeHandles;

namespace BGShell.Components
{
    internal sealed class SharedMemorySection : IDisposable
    {
        private const string PrefixGlobal = "Global\\";
        private const string PrefixLocal = "Local\\";

        private readonly int _size;

        private SafeFileViewHandle _view;
        private SafeFileHandle _mapping;

        public SharedMemorySection(Boolean global, String name, Int32 size, out Boolean createdNew)
        {
            if (global)
            {
                name = PrefixGlobal + name;
            }
            else
            {
                name = PrefixLocal + name;
            }
            
            _mapping = CreateFileMapping(name, size, out createdNew);
            _view = MapFileView(size);
            _size = size;
        }
        
        public void WriteIntPtr(Int32 offset, IntPtr value)
        {
            CheckViewValid();
            CheckOffsetValid(offset, Marshal.SizeOf(value));

            var ptr = _view.DangerousGetHandle();
            Marshal.WriteIntPtr(ptr, offset, value);
        }

        public IntPtr ReadIntPtr(Int32 offset)
        {
            CheckViewValid();
            CheckOffsetValid(offset, Marshal.SizeOf(typeof(IntPtr)));

            var ptr = _view.DangerousGetHandle();
            return Marshal.ReadIntPtr(ptr, offset);
        }

        public void Dispose()
        {
            if (_view != null)
            {
                _view.Dispose();
                _view = null;
            }

            if (_mapping != null)
            {
                _mapping.Dispose();
                _mapping = null;
            }
        }

        private SafeFileHandle CreateFileMapping(String name, Int32 size, out bool createdNew)
        {
            const int ERROR_ALREADY_EXISTS = 183;

            NativeMethods.SetLastError(0);

            var handle = NativeMethods.CreateFileMappingW(
                IntPtr.Zero,
                IntPtr.Zero,
                FileMappingFlags.ReadWrite,
                0, size,
                name);

            if (handle.IsInvalid)
            {
                var hr = Marshal.GetHRForLastWin32Error();

                Dispose();
                throw Marshal.GetExceptionForHR(hr);
            }

            createdNew = (Marshal.GetLastWin32Error() != ERROR_ALREADY_EXISTS);
            return handle;
        }

        private SafeFileViewHandle MapFileView(Int32 size)
        {
            var handle = NativeMethods.MapViewOfFile(
                _mapping,
                FileViewFlags.All,
                0, 0,
                new IntPtr(size));

            if (handle.IsInvalid)
            {
                var hr = Marshal.GetHRForLastWin32Error();

                Dispose();
                throw Marshal.GetExceptionForHR(hr);
            }

            return handle;
        }

        private void CheckViewValid()
        {
            if (_view == null || _view.IsInvalid)
            {
                throw new ObjectDisposedException(GetType().Name);
            }
        }

        private void CheckOffsetValid(Int32 offset, Int32 size)
        {
            if ((offset + size) > _size)
            {
                throw new ArgumentException();
            }
        }
    }
}